<?php
/*
 Autolink Plugin Class for MODx Content Management Framework (http://modxcms.com)
 Copyright (C) 2007 Garry Nutting
 Version: 3.0 Alpha

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

class autolink {
	var $content;
	var $groups;
	var $tblName = 'autolinkwords';
	var $pluginPath = 'assets/plugins/autolink/';
	var $replaceKeyword = true; // replace with keyword instead of match
	var $excludeTags = 'a|code|pre'; // this sits inside a regular expression, so use strict PCRE syntax
	var $templates = array ();
	var $useUTF8 = true; // force autolink into multi-byte encoding mode for regexes

	var $_groups = array ();

	// content
	var $_bodyOpenTag;
	var $_bodyCloseTag;
	var $_head;
	var $_footer;
	var $_zoneArray = array ();
	var $_tokenArray = array ();
	var $_output = '';
	var $_terms = array (); //holds keyword data
	var $_htmlLang = false;
	var $_xmlLang = false;

	/**
	 * PHP4 Class Constructor
	 */
	function autolink($content = false) {
		$this->__construct($content);
	}
	
	/**
	 * Class Constructor
	 */
	function __construct($content = false) {
		global $modx;
		if ($content) {
			$this->content = $content;
		} else {
			$this->content = $modx->documentOutput;
		}
		$this->tblName = $modx->getFullTableName($this->tblName);
	}

	/**
	 * Runs the required autolink methods in sequence for the autolink plugin
	 */
	function run() {
		$this->__getLangAttributes();
		if ($this->getKeywords()) {
			$this->__getBody();
			$this->__splitContent();
			//$this->__loadTemplateClasses();
			$this->__parseTokens();
			return $this->__compileOutput();
		} else {
			return $this->content;
		}
	}

	/**
	 * Loads in the keywords from the database
	 */
	function getKeywords() {
		global $modx;
		//if (count($this->_groups)) {
		//-- get keywords from db
		$rs = $modx->db->select('*', $this->tblName);

		//if no keywords in table, then exit now
		if ($modx->db->getRecordCount($rs) == 0) {
			return $this->content;
		}

		while ($row = $modx->db->getRow($rs)) {
			$this->_terms[$row['keyword']] = $row;
		}
		
		foreach ($this->_terms as $keyword=>$data) {
			//-- see if lang needs to be included
			if ($this->_htmlLang && array_key_exists('lang', $data) && trim($data['lang']) != '' && $this->_htmlLang != $data['lang']) {
				$this->_terms[$keyword]['htmlLang']= true;
			}
			if ($this->_xmlLang && array_key_exists('xmllang', $data) && trim($data['xmllang']) != '' && $this->_xmlLang != $data['xmllang']) {
				$this->_terms[$keyword]['xmlLang']= true;
			}
		}
		return true;
	}

	/**
	 * Retrieves the body content as no matching is required outside of the <body></body> tags
	 */
	function __getBody() {
		if ($this->content) {
			preg_match('/(<body.*?>)/', $this->content, $open);
			preg_match('/(<\/body.*?>)/', $this->content, $close);
			$this->_bodyOpenTag = $open[1];
			$this->_bodyCloseTag = $close[1];
			unset ($open);
			unset ($close);

			//-- get only body html - will add head and footer back in later
			$temp = explode($this->_bodyOpenTag, $this->content);
			$this->_head = $temp[0];
			$this->content = $temp[1];
			$temp = explode($this->_bodyCloseTag, $this->content);
			$this->content = $temp[0];
			$this->_footer = $temp[1];
		}
	}

	/**
	 * Splits the content around the <autolink></autolink> tags
	 */
	function __splitContent() {
		//-- ensure that first autolink match will always be second entry in array
		$this->content = ' ' . $this->content . ' ';

		//-- split the content around the <autolink> tags
		$this->_zoneArray = preg_split("/<autolink>(.*)<\/autolink>/Us", $this->content, -1, PREG_SPLIT_DELIM_CAPTURE);
	}

	/**
	 * This parses the tokens and handles the autolinking of the keywords
	 */
	function __parseTokens() {
		$i = 0;

		foreach ($this->_zoneArray as $zone) {
			$i++;
			//-- skip the entries outside autolink tags
			if ($i % 2 == 1) {
				$this->_output .= $zone;
				continue;
			}

			if ($currentTokens = $this->__tokenize($zone)) {
				$j = 0;
				$tagLevel = 0;
				
				//-- there should always be at least one token, but check just in case
				if (isset ($currentTokens) && is_array($currentTokens) && count($currentTokens) > 0) {
					foreach ($currentTokens as $token) {
						//-- check for a non-tokenised entry which indicates that this token is a tag
						if (!is_array($token)) {								
							//-- check for opening and closing tags
							if (preg_match("/<(?:" . $this->excludeTags . ")\b[^>]*>/i", $token)) { //-- found opening tag
								$tagLevel++;
							}
							elseif (preg_match("#<\s*/\s*(?:" . $this->excludeTags . ")\s*>#i", $token)) { //-- found closing tag
								$tagLevel--;
							}
						}

						//-- check token needs parsing
						if (++ $j % 2 && $token != '' && is_array($token)) {
								foreach ($token as $key => $word) {
									$matched = false;
									foreach ($this->_terms as $keyword => $data) {
										if ($tagLevel == 0 || ($tagLevel>0 && $data['type'] != 'link')) {
											//-- define the regexp that match the keyword in the token
											$encodedMatch = '/\b' . preg_quote(htmlentities($keyword), '/'). '\b/'. ($this->useUTF8 == true ? 'U' : '').'i'; //-- check for encoded match
											$nonencodedMatch = '/\b' . preg_quote($keyword, '/'). '\b/'. ($this->useUTF8 == true ? 'U' : '').'i'; //-- check for non-encoded match
		
											//-- test for encoded keyword
											if (preg_match($encodedMatch, $word, $kmatch) && $matched == false) {
												$substitute = $this->__autolink($keyword,$data);
												$matched = true;
												//-- ensure that the substitute replaces value with the case-sensitive match
												if ($this->replaceKeyword == true) {
													$substitute = preg_replace('/\b' . preg_quote(htmlentities($keyword),'/') . '\b/', $kmatch[0], $substitute);
												}
												//-- replace keyword with the substitute
												$word = preg_replace($encodedMatch, $substitute, $word);
		
												//-- test for non-encoded keyword
											}
											elseif (preg_match($nonencodedMatch, $word, $kmatch) && $matched == false) {
												$substitute = $this->__autolink($keyword,$data);
												$matched = true;
												//-- ensure that the substitute replaces value with the case-sensitive match
												if ($this->replaceKeyword == true) {
													$substitute = preg_replace('/\b' . preg_quote($keyword,'/') . '\b/', $kmatch[0], $substitute);
												}
												//-- replace keyword with the substitute
												$word = preg_replace($nonencodedMatch, $substitute, $word);
											}
									}
									$token[$key] = $word;
								}
							}
						}
						$currentTokens[$j-1] = $token;		
					}
				}
				foreach ($currentTokens as $token) {
					$token = is_array($token) ? implode(" ",$token) : $token;
					$this->_output .= $token;
				}
			} else {
				//log error: Error in __parseTokens()
			}

		}
	}

	/**
	 * Tokenises the zone that has been passed into the function
	 */
	function __tokenize($zone) {
		if (strlen($zone) > 0) {
			//-- this regexp is taken from PHP Markdown by Michel Fortin:
			//-- http://www.michelf.com/projects/php-markdown/
			$comment = '(?s:<!(?:--.*?--\s*)+>)|';
			$processing_instruction = '(?s:<\?.*?\?>)|';
			$tag = '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)';
			$markup = $comment . $processing_instruction . $tag;
			$tokens = preg_split("{($markup)}", $zone, -1, PREG_SPLIT_DELIM_CAPTURE);
			$j = 0;
			foreach ($tokens as $token) {
				if (++$j % 2) {
					$tokens[$j-1] = preg_split('/ /',$token,-1,PREG_SPLIT_DELIM_CAPTURE);
				}
			}
			return $tokens;
		}
	}
	
	/**
	 * Performs the autolinking of the keyword
	 */
	function __autolink($keyword,$data) {
		if (count($this->templates) > 0) {
			foreach ($this->templates as $class) {
				if (!class_exists('al_'.$class) && file_exists(MODX_BASE_PATH . $this->pluginPath . 'templates/' . $class . '/autolink_' . $class . '.class.php')) {
					include_once MODX_BASE_PATH . $this->pluginPath . 'templates/'.$class.'/autolink_' . $class . '.class.php';
					$createClass = 'al_'.$class;
					$class = 'autolink_'.$class;
					${$createClass} = new $class($this->pluginPath);
				}
			}
		}
		$object = 'al_'.$data['type'];
		switch ($data['type']) {
			//-- autolink template functions
			case class_exists('autolink_'.$data['type']) && method_exists(${$object}, $data['replacement']) :
				$substitute = ${$object}->$data['replacement']($keyword);
				break;
				//-- highlight
			case $data['type']=='highlight':
				$substitute = '<span class="autolink_highlight" title="' . $data['title'] . '" style="background-color:' . $data['replacement'] . ';">' . htmlentities($data['keyword']) . '</span>';
				break;
				//-- just link
			case $data['type']=='link':
				$substitute = '<a class="autolink_link" href="' . $data['replacement'] . '" title="' . $data['title'] . '">' . htmlentities($data['keyword']) . '</a>';
				break;
				//-- Acronym or just alt-text
			case $data['type'] == 'abbreviation':
				$substitute = '<abbr title="' . $title . '"><span class="autolink_abbr" title="' . $title . '">' . htmlentities($data['keyword']) . '</span></abbr>';
				break;
			case $data['type'] == 'acronym':
				$substitute = '<acronym class="autolink_acro" title="' . $title . '">' . htmlentities($data['keyword']) . '</acronym>';
				break;
			default:
				$substitute = $keyword;
				break;
		}
		return $substitute;
	}

	/**
	 * Compiles the output to send back to the page
	 */
	function __compileOutput() {
		//-- return the compiled output to the browser
		return $this->_head . $this->_bodyOpenTag . trim($this->_output) . $this->_bodyCloseTag . $this->_footer;
	}

	/**
	 * Extracts the language attributes from the page
	 */
	function __getLangAttributes() {
		if (!$this->_htmlLang) {
			preg_match('/<html.*?lang="(.*?)".*?>/i', $this->_head, $match);
			$this->_htmlLang = $match[0];
		}
		if (!$this->_xmlLang) {
			preg_match('/<html.*?xml:lang="(.*?)".*?>/i', $this->_head, $match);
			$this->_xmlLang = $match[0];
		}
	}
}
?>